This tutorial builds upon the Creating a Full Stack Application with Django, FastAPI, and Next.js guide. We'll integrate Next-Auth (Auth.js) authentication into our existing full-stack application.
1. Initial Setup
If you haven't completed the first tutorial, start by cloning the repository and setting up the initial project.
1.1 Clone the Repository
git clone [email protected]:damianhodgkiss/next-django-fastapi-fullstack-tutorial.git
1.2 Start the Docker Stack
docker compose up --build -d
1.3 Run Database Migrations
docker compose exec api python manage.py migrate
1.4 Create a Superuser
docker compose exec api python manage.py createsuperuser
Follow the prompts to create your superuser account.
Now, let's set up Next-Auth in our Next.js frontend.
2.1 Install Next-Auth
docker compose exec frontend yarn add next-auth@beta
2.2 Create Next-Auth Configuration
Create a new file src/auth.ts
in your frontend directory:
import NextAuth from "next-auth"
export const { handlers, signIn, signOut, auth } = NextAuth({
secret: process.env.NEXTAUTH_SECRET || "secret",
providers: [],
})
2.3 Set Up Next-Auth API Routes
Create a new file src/app/api/auth/[...nextauth]/route.ts
:
import { handlers } from "@/auth";
export const { GET, POST } = handlers;
2.4 Add Next-Auth Middleware
Edit src/middleware.ts
:
export { auth as middleware } from "@/auth";
3. Create Sign In and Sign Out Components
Let's create components for signing in and out, and update our home page.
Create a new file src/app/components/sign-in.tsx
:
import { signIn } from "next-auth/react";
export function SignInButton() {
return <button className="bg-blue-500 py-2 px-4 rounded text-white" onClick={() => signIn()}>Sign in</button>
}
Create a new file src/app/components/sign-out.tsx
:
import { signOut } from "next-auth/react";
export function SignOutButton() {
return <button className="bg-blue-500 py-2 px-4 rounded text-white" onClick={() => signOut()}>Sign out</button>
}
3.3 Update Home Page
Edit src/app/page.tsx
:
import { SignInButton } from "@/components/sign-in";
import { SignOutButton } from "@/components/sign-out";
import { auth } from "@/auth";
import { Sign } from "crypto";
export default async function Home() {
const session = await auth();
const { user } = session || {};
const isSignedIn = !!user;
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
{
isSignedIn ?
<div>
<pre>{JSON.stringify(session, null, 2)}</pre>
<SignOutButton />
</div>
: <SignInButton />
}
</main>
);
}
Now, we'll set up Next-Auth to use Django credentials for authentication.
4.1 Set Environment Variables
Create a frontend/.env.local
file:
NEXTAUTH_SECRET=secret
INTERNAL_API_URL=http://api:8000
4.2 Create Next-Auth Type Declarations
Create a new file frontend/src/types/next-auth.d.ts
:
import NextAuth from "next-auth";
import { JWT } from "next-auth/jwt"
declare module 'next-auth' {
interface User {
first_name: string;
last_name?: string;
is_staff: boolean;
is_active: boolean;
is_superuser: boolean;
last_login: string;
date_joined: string;
}
interface Session {
user: User;
access_token: string;
token_type: 'Bearer';
}
}
declare module 'next-auth/jwt' {
interface JWT {
user: User;
access_token: string;
token_type: 'Bearer';
}
}
4.3 Update Next-Auth Configuration
Update frontend/src/auth.ts
:
import NextAuth, { type User } from "next-auth";
import { CredentialsSignin } from '@auth/core/errors';
import CredentialsProvider from 'next-auth/providers/credentials';
export const { handlers, signIn, signOut, auth } = NextAuth({
secret: process.env.NEXTAUTH_SECRET || "secret",
providers: [
CredentialsProvider({
id: 'django',
name: 'Django',
credentials: {
username: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
const response = await fetch(`${process.env.INTERNAL_API_URL}/users/login/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(credentials),
});
const json = await response.json();
if (!response.ok) throw new CredentialsSignin(json.detail);
return json;
}
})
],
callbacks: {
async jwt({ token, user, account }) {
switch (account?.provider) {
case 'django':
const credentialUser = user as any;
token.access_token = credentialUser?.access_token;
token.token_type = credentialUser?.token_type;
token.user = credentialUser?.user;
break;
}
return token;
},
async session({ session, token }) {
const accessToken = token?.access_token;
const expireSession = {
expires: new Date().toISOString(),
};
if (!accessToken) {
return expireSession;
}
const response = await fetch(`${process.env.INTERNAL_API_URL}/users/session/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
});
if (response.status !== 200) {
return expireSession;
}
const json = await response.json();
return {
...session,
access_token: json.access_token,
token_type: json.token_type,
user: json.user,
};
}
}
})
4.4 Update Docker Compose Configuration
Edit docker-compose.yml
:
frontend:
env_file:
- ./frontend/.env.local
4.5 Restart Frontend Container
docker compose up frontend -d
5. Create API Routes for Django Authentication
Now, we'll create the necessary API routes in our Django backend.
5.1 Update Backend Requirements
Edit backend/requirements.txt
:
5.2 Create User Schemas
Create a new file backend/users/schemas.py
:
from pydantic import BaseModel
from pydantic import ConfigDict
from typing import Optional
from datetime import datetime
class LoginRequest(BaseModel):
username: str
password: str
class UserSchema(BaseModel):
id: int
email: str
first_name: Optional[str] = None
last_name: Optional[str] = None
name: Optional[str] = None
is_staff: bool
is_active: bool
is_superuser: bool
last_login: datetime | None
date_joined: datetime
model_config = ConfigDict(from_attributes=True)
class Token(BaseModel):
access_token: str
token_type: str
user: UserSchema
5.3 Create Authentication Utilities
Create a new file backend/users/utils.py
:
from fastapi import HTTPException, status, Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from datetime import datetime, timedelta, timezone
from jose import jwt, JWTError
from django.conf import settings
from django.contrib.auth import get_user_model
from typing import Any, Annotated
from .models import User
SECRET_KEY = settings.SECRET_KEY
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 30
ACCESS_TOKEN_VALID_MINUTES = 1
security = HTTPBearer()
optional_security = HTTPBearer(auto_error=False)
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
to_encode = data.copy()
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(minutes=15)
to_encode.update({"exp": expire})
token = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return token
async def get_access_token(
credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)]
) -> str:
if not credentials:
raise credentials_exception
try:
return credentials.credentials
except JWTError:
raise credentials_exception
async def get_token_payload(
token: Annotated[str, Depends(get_access_token)]
) -> dict[str, Any]:
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
id: str | None = payload.get("sub")
if id is None:
raise credentials_exception
return payload
except JWTError:
raise credentials_exception
async def get_current_user(
payload: Annotated[dict[str, Any], Depends(get_token_payload)]
) -> User:
try:
id: str | None = payload.get("sub")
if id is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = await get_user_model().objects.filter(id=id, is_active=True).afirst()
if user is None:
raise credentials_exception
return user
async def get_optional_current_user(
credentials: Annotated[HTTPAuthorizationCredentials, Depends(optional_security)]
) -> User | None:
if not credentials:
return None
try:
payload = await get_token_payload(credentials.credentials)
return await get_current_user(payload)
except HTTPException:
return None
async def get_current_active_user(
current_user: Annotated[User, Depends(get_current_user)],
) -> User:
if not current_user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
5.4 Create Authentication Routers
Create a new file backend/users/routers.py
:
from fastapi import FastAPI, APIRouter, Depends
from django.contrib.auth import authenticate
from typing import Annotated, Any
from datetime import timedelta
from .schemas import LoginRequest, Token
from datetime import datetime
from .utils import (
create_access_token,
get_access_token,
get_token_payload,
get_current_active_user,
credentials_exception,
ACCESS_TOKEN_EXPIRE_MINUTES,
ACCESS_TOKEN_VALID_MINUTES,
)
from .models import User
router = APIRouter(prefix="/users", tags=["users"])
@router.post("/login/")
def login(login: LoginRequest):
user = authenticate(email=login.username, password=login.password)
if not user:
raise credentials_exception
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": str(user.id)}, expires_delta=access_token_expires
)
return Token(access_token=access_token, token_type="Bearer", user=user)
@router.post("/session/")
async def check_session(
token: Annotated[str, Depends(get_access_token)],
payload: Annotated[dict[str, Any], Depends(get_token_payload)],
current_user: Annotated[User, Depends(get_current_active_user)],
) -> Token:
exp_time = datetime.fromtimestamp(payload["exp"])
current_time = datetime.now()
time_difference = exp_time - current_time
difference_in_minutes = time_difference.total_seconds() / 60
if difference_in_minutes >= ACCESS_TOKEN_VALID_MINUTES:
return Token(access_token=token, token_type="Bearer", user=current_user)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": str(current_user.id)}, expires_delta=access_token_expires
)
return Token(access_token=access_token, token_type="bearer", user=current_user)
def register_routers(app: FastAPI):
app.include_router(router)
5.5 Update ASGI Configuration
Update backend/mysaas/asgi.py
:
import os
from django.core.asgi import get_asgi_application
from fastapi import FastAPI
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysaas.settings")
application = get_asgi_application()
fastapp = FastAPI(
servers=[
{
"url": "/api/v1",
"description": "V1",
}
]
)
def init(app: FastAPI):
from users.routers import register_routers as register_user_routers
register_user_routers(app)
@app.get("/health")
def health_check():
return {"status": "ok"}
init(fastapp)
6. Verify the Setup
To verify that everything is set up correctly:
- Visit http://localhost/ - You should see a sign-in button.
- Visit http://localhost/docs - You should see the /users/login/ endpoint in the FastAPI documentation.
- Visit http://localhost/admin - You should be able to log in to the Django admin site using the superuser credentials you created earlier.
7. Protecting API Routes
To protect API routes, use the get_current_active_user
function in your FastAPI route definitions:
@router.post("/my-api-route/")
async def my_api_route(
current_user: Annotated[User, Depends(get_current_active_user)],
):
# user is authenticated as current_user
...
When making requests to protected routes from the frontend, include the access token in the Authorization header:
import { auth } from "@/auth";
const session = await auth();
// ...check session.access_token is not null
const response = await fetch(`${process.env.INTERNAL_API_URL}/.../my-api-route/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${session.access_token}`,
},
body: JSON.stringify(data),
});
// ...check response.ok or response.status
This setup ensures that the access token from the Next-Auth session is passed to the API route, allowing the backend to authenticate the user. The get_current_active_user
function will verify the token and retrieve the corresponding user, making it available in your route handler.
By using this approach, you can easily secure any API route that requires authentication, ensuring that only authenticated users can access these endpoints.
Conclusion
You have now successfully integrated Next-Auth authentication into your Django, FastAPI, and Next.js stack. This setup allows users to authenticate using Django credentials through a Next.js frontend, with FastAPI handling the API requests. Remember to implement additional security measures and features like password reset or email verification as needed for your specific application.